package com.starsky.common.redis.utils;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import redis.clients.jedis.Jedis;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;

/**
 * @author wangsh
 * @desc redis分布式服务加锁/解锁
 * 以下使用的是基于底层lua实现枷锁、解锁，其他使用redistemp.setnx和expire操作及redissession等都不能实现原子操作。
 * @email 1057718341@qq.com
 * @date 2021-09-17
 */
public class RedisLockUtils {
    private static Log log = LogFactory.getLog(RedisLockUtils.class);
    //枷锁结果
    private static final String LOCK_SUCCESS = "OK";
    //NX即 SET IF NOT EXIST，即当key不存在时，我们进行set操作；若key已经存在，则不做任何操作
    private static final String SET_IF_NOT_EXIST = "NX";
    //PX即expire, 意思是我们要给这个key加一个过期的设置，具体过期时间参数决定
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    //结果
    private static final Long SUCCESS = 1L;


    /***********************第一种：该种方式是正确的操作********************************/
    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识，可以使用UUID.randomUUID().toString()方法生成
     * @param expireTime 超期时间,毫秒值
     * @return 是否获取成功
     */
    public static boolean tryLock(Jedis jedis, String lockKey, String requestId, Integer expireTime) {
        //这里使用底层lua实现，将set 和expire 两条命名放在一起执行，保证原子操作
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        return Objects.equals(LOCK_SUCCESS, result);
    }

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识，即枷锁时的requestId标识
     * @return 是否释放成功
     */
    public static boolean reLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return Objects.equals(SUCCESS, result);
    }

    /**
     * 尝试获取分布式锁
     * 参考文档：https://blog.csdn.net/qq_34845394/article/details/102622146
     *
     * @param redisTemplate Redis客户端
     * @param lockKey       锁
     * @param requestId     请求标识，可以使用UUID.randomUUID().toString()方法生成
     * @param expireTime    超期时间,毫秒值
     * @return 是否获取成功
     */
    public static boolean tryLock(RedisTemplate redisTemplate, String lockKey, String requestId, Integer expireTime) {
        //这里使用使用脚本调用底层lua实现，将set 和expire 两条命名放在一起执行，保证原子操作
//        String scriptStr = "if redis.call('set',KEYS[1],ARGV[1],'NX','PX',ARGV[2]) then return 1 else return 0 end";
        String scriptStr = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('expire', KEYS[1], ARGV[2]) return 1 else return 0 end";
//        String scriptStr = "if redis.call('setNx', KEYS[1], ARGV[1]) then if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end end";
        RedisScript redisScript = RedisScript.of(scriptStr, Long.class);
        Object res = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId, expireTime);
        log.warn("tryLock executeResult: " + res);
        return Objects.equals(SUCCESS, res);
    }

    /**
     * 释放分布式锁
     *
     * @param redisTemplate Redis客户端
     * @param lockKey       锁
     * @param requestId     请求标识，即枷锁时的requestId标识
     * @return 是否释放成功
     */
    public static boolean reLock(RedisTemplate redisTemplate, String lockKey, String requestId) {

        String scriptStr = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript<ArrayList<String>> redisScript = new DefaultRedisScript(scriptStr, ArrayList.class);
        Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        log.warn("unLock executeResult: " + result);
        return Objects.equals(SUCCESS, result);
    }


    /*********************第二种：大多数使用该方式，但是该方式也不能保证原子操作，相对来说这种还是可以使用*********************/
    /**
     * @param jedis
     * @param lockKey    锁
     * @param expireTime 过期时间,毫秒值
     * @desc 以下问题：
     * 1. 由于是客户端自己生成过期时间，所以需要强制要求分布式下每个客户端的时间必须同步。
     * 2. 当锁过期的时候，如果多个客户端同时执行 getSet()方法，那么虽然最终只有一个客户端可以加锁，
     * 但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识，即任何客户端都可以解锁
     */
    public static boolean tryLock2(Jedis jedis, String lockKey, Integer expireTime) {

        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);

        // 如果当前锁不存在，返回加锁成功
        if (jedis.setnx(lockKey, expiresStr) == 1) {
            return true;
        }

        // 如果锁存在，获取锁的过期时间
        String currentValueStr = jedis.get(lockKey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            // 锁已过期，获取上一个锁的过期时间，并设置现在锁的过期时间
            String oldValueStr = jedis.getSet(lockKey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                // 考虑多线程并发的情况，只有一个线程的设置值和当前值相同，它才有权利加锁
                return true;
            }
        }
        // 其他情况，一律返回加锁失败
        return false;
    }

    /**
     * @param jedis
     * @param lockKey
     * @param requestId
     * @desc <p>
     * 如下问题在于如果调用 jedis.del()方法的时候，这把锁已经不属于当前客户端的时候会解除他人加的锁。
     * 那么是否真的有这种场景？答案是肯定的，比如客户端A加锁，一段时间之后客户端A解锁，在执行jedis.del()之前，
     * 锁突然过期了，此时客户端B尝试加锁成功，然后客户端A再执行delete()方法，则将客户端B的锁给解除了
     * </p>
     */
    public static void reLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时，这把锁突然不是这个客户端的，则会误解锁
            jedis.del(lockKey);
        }
    }

    /**
     * @param template
     * @param lockKey    锁
     * @param expireTime 过期时间,毫秒值
     * @desc 以下问题：
     * 1. 由于是客户端自己生成过期时间，所以需要强制要求分布式下每个客户端的时间必须同步。
     * 2. 当锁过期的时候，如果多个客户端同时执行 getAndSet()方法，那么虽然最终只有一个客户端可以加锁，
     * 但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识，即任何客户端都可以解锁
     */
    public static boolean tryLock2(RedisTemplate template, String lockKey, Integer expireTime) {

        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);

        // 如果当前锁不存在，返回加锁成功
        if (template.opsForValue().setIfAbsent(lockKey, expires)) {
            return true;
        }

        Object currentValue = template.opsForValue().get(lockKey);
        //判断锁是否过期
        if ((currentValue == null || "".equals(currentValue))
                || Long.parseLong(currentValue.toString()) < System.currentTimeMillis()) {
            //获取上一个过期时间
            String oldValue = template.opsForValue().getAndSet(lockKey, expires).toString();
            if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param template
     * @param lockKey  锁
     * @desc <p>
     * 如下问题在于如果调用 template.delete()方法的时候，这把锁已经不属于当前客户端的时候会解除他人加的锁。
     * 那么是否真的有这种场景？答案是肯定的，比如客户端A加锁，一段时间之后客户端A解锁，在执行jedis.del()之前，
     * 锁突然过期了，此时客户端B尝试加锁成功，然后客户端A再执行delete()方法，则将客户端B的锁给解除了
     * </p>
     */
    public static boolean reLock2(RedisTemplate template, String lockKey) {
        // 若在此时，这把锁突然不是这个客户端的，则会误解锁
        return template.delete(lockKey);
    }
/*************************************************************************************/

}